﻿using System;
using System.Collections.Generic;
using NCommet.Core.Agents;
using System.Collections.ObjectModel;

namespace NCommet.Core.Services
{
    /// <summary>
    /// HierarchyManager is a static class containing methods that query the item hierarchy.
    /// </summary>
    public static class HierarchyManager
    {
        private static bool MatchItemTypes(Item Item, string[] ItemTypes)
        {
            foreach (string ItemType in ItemTypes)
                if (Item.ItemType == ItemType)
                    return true;
            return false;
        }

        private static bool MatchItemTags(Item Item, string[] Tags)
        {
            foreach (string Tag in Tags)
                if (Item.Tags.Contains(Tag))
                    return true;
            return false;
        }

        #region Methods for retreiving Root Items

        /// <summary>
        /// Gets all root items, using the persister.
        /// </summary>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see>.</returns>
        public static IList<Item> GetRoots()
        {
            IPersister persister = NCommetContainer.Instance.Persister;
            return persister.GetRoots();
        }

        /// <summary>
        /// Gets all root items of the specified <paramref name="ItemType"/>, using the persister.
        /// </summary>
        /// <param name="ItemType">The item type that is used as the filter.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see>.</returns>
        public static IList<Item> GetRoots(string ItemType)
        {
            IPersister persister = NCommetContainer.Instance.Persister;
            return persister.GetRoots(ItemType);
        }

        /// <summary>
        /// Gets all root items whose item type is included in <paramref name="ItemTypes"/>, using the persister.
        /// </summary>
        /// <param name="ItemTypes">The array with the item types that are used as the filter.</param>
        /// <returns>List of <see cref="NCommet.Core.Item">Items</see>.</returns>
        public static IList<Item> GetRoots(string[] ItemTypes)
        {
            IPersister persister = NCommetContainer.Instance.Persister;
            return persister.GetRoots(ItemTypes);
        }

        #endregion

        #region Methods for retreiving Item ancestors and ancestor paths

        /// <summary>
        /// Returns the ancestor of an Item that is a specified number of levels up in the hierarchy.
        /// </summary>
        /// <param name="Item">The <see cref="Item"/> whose ancestor will be returned.</param>
        /// <param name="Level">A number indicating the distance between the <paramref name="Item"/> and the ancestor that will be returned.</param>
        /// <returns>An <see cref="Item"/> instance that is an ancestor of the specified <paramref name="Item"/> and that is <paramref name="Level"/> hops away from it, 
        /// or null if <paramref name="Level"/> is greater than the current Item's level.</returns>
        /// <exception cref="ArgumentException">
        /// If <paramref name="Level"/> is lesser of equal to zero.
        /// </exception>
        public static Item GetAncestor(Item Item, int Level)
        {
            if (Level <= 0)
                throw new ArgumentException("Level must be a positive number", "Level");
            Item p = Item;
            while (p != null && Level > 0)
            {
                p = p.Parent;
                Level--;
            }
            return p;
        }

        /// <summary>
        /// Gets the closest ancestor of the given item whose item type equals <paramref name="ItemType"/>.
        /// </summary>
        /// <param name="Item">The given item.</param>
        /// <param name="ItemType">The type of the closest ancestor.</param>
        /// <returns>The closest ancestor of the given <paramref name="Item"/> with type <paramref name="ItemType"/></returns>
        public static Item GetAncestor(Item Item, string ItemType)
        {
            Item p = Item.Parent;
            while (p != null && p.ItemType != ItemType)
                p = p.Parent;
            return p;
        }

        /// <summary>
        /// Gets the closest ancestor of the given item whose item type is included in <paramref name="ItemTypes"/>.
        /// </summary>
        /// <param name="Item">The given item.</param>
        /// <param name="ItemTypes">The types of the closest ancestor.</param>
        /// <returns>The closest ancestor of the given <paramref name="Item"/> with type that is included in <paramref name="ItemTypes"/></returns>
        public static Item GetAncestor(Item Item, string[] ItemTypes)
        {
            Item p = Item.Parent;
            while (p != null && !MatchItemTypes(p, ItemTypes))
                p = p.Parent;
            return p;
        }

        /// <summary>
        /// Creates a path starting from the given <paramref name="Item"/> and navigating upwards to the parent of each node.
        /// Path stops when an item is found that doesn't meet the <paramref name="Filters"/>.
        /// The resulting list starts from the highest found ancestor.
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>
        /// <param name="Filters">Filters to determine when the path ends. <seealso cref="FilterCollection"/></param>
        /// <param name="IncludeSelf">Determines if <paramref name="Item"/> is included in the returned path.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        /// <exception cref="NullReferenceException">
        /// If <paramref name="Item"/> is null.
        /// </exception>
        private static IList<Item> GetAncestorPath(Item Item, FilterCollection Filters, bool IncludeSelf)
        {
            if (Item == null)
                throw new NullReferenceException("GetAncestorPath cannot handle null item");
            List<Item> result = new List<Item>();
            if (IncludeSelf)
                result.Add(Item);
            Item p = Item.Parent;
            int level = 1;
            while (p != null)
            {
                if (Filters != null)
                {
                    if (Filters.MaxLevel > 0 && level > Filters.MaxLevel)
                        break;
                    if (!Filters.Match(p))
                        break;
                }

                result.Insert(0, p);
                p = p.Parent;
                level++;
            }
            return result;
        }

        /// <summary>
        /// Returns the ancestor path of the given <paramref name="Item"/>.
        /// The path is formed by navigating from each node upwards to its parent.
        /// The path ends when either one of the following conditions are met:
        /// <list type="bullet">
        ///   <item>
        ///     <description>If the <paramref name="Level"/> is positive, path formation stops when its length exceeds <paramref name="Level"/>.</description>
        ///   </item>
        ///   <item>
        ///     <description>If <paramref name="ItemTypes"/> is not null, path formation stops when an item is found whose item type is not included in <paramref name="ItemTypes"/>.</description>
        ///   </item>
        /// </list>
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>
        /// <param name="ItemTypes">An array of item types that, if one is found in the forming path, the path is immediately ended.</param>
        /// <param name="Level">An integer, that if positive, causes the forming path to end when its length exceeds it.</param>
        /// <param name="IncludeSelf">Determines if <paramref name="Item"/> is included in the returned path.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        /// <exception cref="NullReferenceException">
        /// If <paramref name="Item"/> is null.
        /// </exception>
        public static IList<Item> GetAncestorPath(Item Item, string[] ItemTypes, int Level, bool IncludeSelf)
        {
            return HierarchyManager.GetAncestorPath(Item, new FilterCollection(ItemTypes, null, Level), IncludeSelf);
        }

        /// <summary>
        /// Returns the ancestor path of the given <paramref name="Item"/>, not including the item itself.
        /// The path starts from the highest ancestor.
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        public static IList<Item> GetAncestorPath(Item Item)
        {
            return HierarchyManager.GetAncestorPath(Item, new FilterCollection(null, null, 0), false);
        }

        /// <summary>
        /// Returns the ancestor path of the given <paramref name="Item"/>, not including the item itself.
        /// The path stops at the first ancestor that doesn't match the given item type.
        /// The resulting list is ordered from the highest ancestor.
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>
        /// <param name="ItemType">An item type that, if found in the forming path, the path is immediately ended.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        /// <exception cref="NullReferenceException">
        /// If <paramref name="Item"/> is null.
        /// </exception>
        public static IList<Item> GetAncestorPath(Item Item, string ItemType)
        {
            return HierarchyManager.GetAncestorPath(Item, new FilterCollection(new string[] { ItemType }, null, 0), false);
        }

        /// <summary>
        /// Returns the ancestor path of the given <paramref name="Item"/>, not including the item itself.
        /// If <paramref name="Level"/> is positive, path formation stops when its length is equal to the parameter.
        /// Also, path formation stops at the first ancestor whose item type doesn't match <paramref name="ItemType"/>.
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>
        /// <param name="ItemType">An item type that, if found in the forming path, the path is immediately ended.</param>
        /// <param name="Level">An integer, that if positive, causes the forming path to end when its length exceeds it.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        /// <exception cref="NullReferenceException">
        /// If <paramref name="Item"/> is null.
        /// </exception>
        public static IList<Item> GetAncestorPath(Item Item, string ItemType, int Level)
        {
            return HierarchyManager.GetAncestorPath(Item, new FilterCollection(new string[] { ItemType }, null, Level), false);
        }

        /// <summary>
        /// Returns the ancestor path of the given <paramref name="Item"/>, optionally including the item itself.
        /// The resulting list is ordered from the highest ancestor.
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>        
        /// <param name="IncludeSelf">Determines if <paramref name="Item"/> is included in the returned path.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        /// <exception cref="NullReferenceException">
        /// If <paramref name="Item"/> is null.
        /// </exception>
        public static IList<Item> GetAncestorPath(Item Item, bool IncludeSelf)
        {
            return HierarchyManager.GetAncestorPath(Item, new FilterCollection(null, null, 0), IncludeSelf);
        }

        /// <summary>
        /// Returns the ancestor path of the given <paramref name="Item"/>, optionally including the item itself.
        /// The path stops at the first ancestor that doesn't match the given item type.
        /// The resulting list is ordered from the highest ancestor.
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>
        /// <param name="ItemType">An item type that, if found in the forming path, the path is immediately ended.</param>
        /// <param name="IncludeSelf">Determines if <paramref name="Item"/> is included in the returned path.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        /// <exception cref="NullReferenceException">
        /// If <paramref name="Item"/> is null.
        /// </exception>
        public static IList<Item> GetAncestorPath(Item Item, string ItemType, bool IncludeSelf)
        {
            return HierarchyManager.GetAncestorPath(Item, new FilterCollection(new string[] { ItemType }, null, 0), IncludeSelf);
        }

        /// <summary>
        /// Return the ancestor path of the given <paramref name="Item"/>, optionally including the item itself.
        /// If <paramref name="Level"/> is positive, path stops when its length is equal to the parameter (path length
        /// without the optionally added item).
        /// </summary>
        /// <param name="Item">The item whose ancestor path is retrieved.</param>
        /// <param name="Level">An integer, that if positive, causes the forming path to end when its length exceeds it.</param>
        /// <param name="IncludeSelf">Determines if <paramref name="Item"/> is included in the returned path.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see> that belong to the requested path.</returns>
        /// <exception cref="NullReferenceException">
        /// If <paramref name="Item"/> is null.
        /// </exception>
        public static IList<Item> GetAncestorPath(Item Item, int Level, bool IncludeSelf)
        {
            return HierarchyManager.GetAncestorPath(Item, new FilterCollection(null, null, Level), IncludeSelf);
        }

        /// <summary>
        /// Checks if <paramref name="testParent"/> is an ancestor of <paramref name="testChild"/>.
        /// </summary>
        /// <param name="testParent">The item that is tested to be the parent.</param>
        /// <param name="testChild">The item that is tested to be the child.</param>
        /// <returns>True, if <paramref name="testParent"/> is ancestor of <paramref name="testChild"/>.</returns>
        /// <exception cref="ArgumentException">
        /// <ul>
        /// <li>If <paramref name="testParent"/> is null.</li>
        /// <li>If <paramref name="testChild"/> is null.</li>
        /// </ul>
        /// </exception>
        public static bool HasAncestor(Item testParent, Item testChild)
        {
            if (testParent == null)
                throw new ArgumentException("TestParent cant be null", "testParent");
            if (testChild == null)
                throw new ArgumentException("TestChild cant be null", "testChild");
            Item p = testChild.Parent;
            while (p != null && p != testParent)
                p = p.Parent;
            return p == testParent;
        }

        #endregion

        #region Methods for retreiving an Item hierarchy according to specific filters

        private static Node GetHierarchy(Item Item, FilterCollection Filters, bool Preload)
        {
            return new Node(Item, Filters, Preload);
        }

        /// <summary>
        /// Returns an item hierarchy filtered by the specified parameters. The hierarchy is not preloaded, i.e.
        /// filtering is performed when needed.
        /// </summary>
        /// <param name="Item">The root item of the hierarchy.</param>
        /// <param name="ItemTypes">If not null, only include items of the given item types.</param>
        /// <returns>The item hierarchy.</returns>
        public static Node GetHierarchy(Item Item, string[] ItemTypes)
        {
            return HierarchyManager.GetHierarchy(Item, new FilterCollection(ItemTypes, null, 0), false);
        }

        /// <summary>
        /// Returns an item hierarchy filtered by the specified parameters. The hierarchy is not preloaded, i.e.
        /// filtering is performed when needed.
        /// </summary>
        /// <param name="Item">The root item of the hierarchy.</param>
        /// <param name="ItemTypes">If not null, only include items of the given item types.</param>
        /// <param name="Tags">If not null, only include items that are tagged by at least one tag included in this parameter.</param>
        /// <returns>The item hierarchy.</returns>
        public static Node GetHierarchy(Item Item, string[] ItemTypes, string[] Tags)
        {
            return HierarchyManager.GetHierarchy(Item, new FilterCollection(ItemTypes, Tags, 0), false);
        }

        /// <summary>
        /// Returns an item hierarchy filtered by the specified parameters. The hierarchy is not preloaded, i.e.
        /// filtering is performed when needed.
        /// </summary>
        /// <param name="Item">The root item of the hierarchy.</param>
        /// <param name="ItemTypes">If not null, only include items of the given item types.</param>
        /// <param name="Tags">If not null, only include items that are tagged by at least one tag included in this parameter.</param>
        /// <param name="MaxLevel">If positive, limit the hierarchy to the specified height.</param>
        /// <returns>The item hierarchy.</returns>
        public static Node GetHierarchy(Item Item, string[] ItemTypes, string[] Tags, int MaxLevel)
        {
            return HierarchyManager.GetHierarchy(Item, new FilterCollection(ItemTypes, Tags, MaxLevel), false);
        }

        /// <summary>
        /// Returns an item hierarchy filtered by the specified parameters.
        /// </summary>
        /// <param name="Item">The root item of the hierarchy.</param>
        /// <param name="ItemTypes">If not null, only include items of the given item types.</param>
        /// <param name="Tags">If not null, only include items that are tagged by at least one tag included in this parameter.</param>
        /// <param name="MaxLevel">If positive, limit the hierarchy to the specified height.</param>
        /// <param name="Preload">If true, preload all items in the hierarchy.</param>
        /// <returns>The item hierarchy.</returns>
        public static Node GetHierarchy(Item Item, string[] ItemTypes, string[] Tags, int MaxLevel, bool Preload)
        {
            return HierarchyManager.GetHierarchy(Item, new FilterCollection(ItemTypes, Tags, MaxLevel), Preload);
        }

        #endregion

        #region Nested Classes for managing and filtering an Item Hierarchy

        /// <summary>
        /// A node is a part of a filtered item hierarchy.
        /// </summary>
        public class Node
        {
            private FilterCollection filters;

            private Item item;
            /// <summary>
            /// Gets the item that is related to this node.
            /// </summary>
            public Item Item { get { return item; } }

            private int relativeLevel;

            /// <summary>
            /// The relative level of this node. The highest node that was created has a relative level of zero.
            /// </summary>
            public int RelativeLevel { get { return relativeLevel; } }

            private ReadOnlyCollection<Node> nodes;

            /// <summary>
            /// Contains the children nodes of this node. In the item hierarchy, that represents the filtered children
            /// of the item related to this node.
            /// Children are loaded on the fly, unless a preloaded hierarchy was requested at <see cref="HierarchyManager" />.
            /// </summary>
            public ReadOnlyCollection<Node> Nodes
            {
                get
                {
                    if (this.nodes == null)
                        this.ApplyFilters(false);
                    return nodes;
                }
            }

            /// <summary>
            /// Applies the filters in the list of children of the Item to select only the appropriate Items to load in the Nodes collection
            /// </summary>
            /// <param name="PreloadHierarchy">If true, the hierarchy is preloaded.</param>
            /// <remarks>The method uses the fields of the <see cref="filters"/> and the <see cref="Item"/> as a root to select matching Items to be used
            /// to fill the <see cref="Nodes"/>.</remarks>
            private void ApplyFilters(bool PreloadHierarchy)
            {
                if (filters.MaxLevel > 0 && this.relativeLevel > filters.MaxLevel)
                {
                    this.nodes = new ReadOnlyCollection<Node>(new List<Node>());
                }
                else
                {
                    IPersister persister = NCommetContainer.Instance.Persister;
                    IList<Item> filtered = persister.FilterChildren(item, filters.ItemTypes, filters.Tags);
                    IList<Node> temp = new List<Node>();
                    foreach (Item it in filtered)
                        temp.Add(new Node(it, filters, PreloadHierarchy));
                    this.nodes = new ReadOnlyCollection<Node>(temp);
                }
            }


            private Node(Item item, FilterCollection Filters, bool PreloadHierarchy, int relativeLevel)
            {
                this.item = item;
                this.filters = Filters;
                this.relativeLevel = relativeLevel;
                if (PreloadHierarchy)
                    this.ApplyFilters(true);
            }

            /// <summary>
            /// Creates a new <c>Node</c> instance.
            /// </summary>
            /// <param name="item">The item that is related to the node.</param>
            /// <param name="Filters">The filters to apply to the item's children.</param>
            /// <param name="PreloadHierarchy">If true, the hierarchy is preloaded.</param>
            internal Node(Item item, FilterCollection Filters, bool PreloadHierarchy) : this(item, Filters, PreloadHierarchy, 0) { }
        }

        /// <summary>
        /// Contains a defined set of filters that are useful when filtering Item instances.
        /// </summary>
        /// <remarks>This class is only intended to be used inside the HierarchyManager class as a filters' container. It exposes properties to check which filters have been defined and exposes these filters.</remarks>
        internal class FilterCollection
        {
            public string[] ItemTypes;
            public string[] Tags;
            public int MaxLevel;

            public FilterCollection(string[] ItemTypes, string[] Tags, int MaxLevel)
            {
                this.ItemTypes = ItemTypes;
                this.Tags = Tags;
                this.MaxLevel = MaxLevel;
            }

            public bool Match(Item Item)
            {
                if (ItemTypes == null || ItemTypes.Length == 0)
                {
                    if (Tags == null || Tags.Length == 0)
                        return true;
                    else
                        return MatchItemTags(Item, Tags);
                }
                else
                {
                    if (Tags == null || Tags.Length == 0)
                        return MatchItemTypes(Item, ItemTypes);
                    else
                        return MatchItemTypes(Item, ItemTypes) && MatchItemTags(Item, Tags);
                }
            }
        }

        #endregion

        /// <summary>
        /// Searches for children of the given item types, reaching up to MaxLevel depth to find them.
        /// </summary>
        /// <param name="parent">The parent item.</param>
        /// <param name="ItemTypes">An array of item types that are used as a filter for the search.</param>
        /// <param name="MaxLevel">The max search depth. If it is zero it is ignored.</param>
        /// <returns>A list of <see cref="NCommet.Core.Item">Items</see>.</returns>
        public static IList<Item> FindChildren(Item parent, string[] ItemTypes, int MaxLevel)
        {
            int currentLevel = 0;
            Queue<Item> q = new Queue<Item>();
            q.Enqueue(parent);
            List<Item> result = new List<Item>();
            while (q.Count > 0 && (MaxLevel <= 0 || ++currentLevel <= MaxLevel))
            {
                Item i = q.Dequeue();
                foreach (Item child in i.Children)
                {
                    q.Enqueue(child);

                    if (MatchItemTypes(child, ItemTypes))
                        result.Add(child);
                }
            }
            return result;
        }

        /// <summary>
        /// Returns the count of items in the subtree designated by the given node, inclusive.
        /// </summary>
        /// <param name="root">The root item.</param>
        /// <returns>A count of the items in the subtree.</returns>
        public static int TotalItemCount(Item root)
        {
            int result = 1;
            foreach (Item child in root.Children)
                result += TotalItemCount(child);
            return result;
        }
    }
}
